﻿//-----------------------------------------------------------------------
// <copyright file="StorageManager.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>15/07/2012</date>
//-----------------------------------------------------------------------

using ConsoleHoster.Common.Model;
using ConsoleHoster.Common.Utilities;
using ConsoleHoster.Common.Utilities.IO;
using ConsoleHoster.Model.Entities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace ConsoleHoster.Model
{
    public sealed class StorageManager : IStorageManager
    {
        private const string TAG_GLOBAL_SETTINGS = "GlobalSettings";
        private const string TAG_FONT_FAMILY = "FontFamily";
        private const string TAG_PROJECT = "Project";
        private const string TAG_PROJECTS = "Projects";
        private const string TAG_COMMAND = "Command";
        private const string TAG_GLOBAL_COMMANDS = "GenericCommands";
        private const string TAG_HOSTER_CONFIG = "HosterConfig";
        private const string TAG_PLUGINS = "Plugins";

        private const string ATTRIBUTE_USE_BOLD_FONTS = "UseBoldFonts";

        private readonly IFileSystemContentProvider directoryContentProvider;
        private readonly IXmlStorage xmlStorage;
        private readonly ILogger logger;

        public StorageManager(IFileSystemContentProvider argDirectoryContentProvider, IXmlStorage argXmlStorage, ILogger argLogger)
        {
            ValidationManager.RequireArgumentNotNull(argDirectoryContentProvider, "argDirectoryContentProvider");
            ValidationManager.RequireArgumentNotNull(argXmlStorage, "argXmlStorage");

            this.directoryContentProvider = argDirectoryContentProvider;
            this.xmlStorage = argXmlStorage;
            this.logger = new ComponentLogger("StorageManager", argLogger);
        }

        public void ExtractProjects(IEnumerable<ConsoleProject> argProjects, string argPath)
        {
            XDocument tmpDoc = XDocument.Parse(@"<?xml version='1.0' encoding='utf-8' ?>
  <" + TAG_PROJECTS + "></" + TAG_PROJECTS + ">");
            XElement tmpProjectsRootElement = tmpDoc.Descendants(TAG_PROJECTS).Single();
            foreach (ConsoleProject tmpProject in argProjects)
            {
                XElement tmpProjectElement = tmpProject.ToXElement();
                AddProjectCommandsToXElement(tmpProject.Commands, tmpProjectElement);
                tmpProjectsRootElement.Add(tmpProjectElement);
            }

            this.xmlStorage.StoreXDocument(tmpDoc, argPath);
        }

        public IEnumerable<ConsoleProject> LoadProjects(string argPath)
        {
            IList<ConsoleProject> tmpResult = new List<ConsoleProject>();
            XDocument tmpProjectsDocument = this.GetSettingsXmlDocument(argPath);
            foreach (XElement tmpProjectXml in tmpProjectsDocument.Descendants(TAG_PROJECT))
            {
                ConsoleProject tmpProject = ConsoleProject.LoadFromXElement(tmpProjectXml);
                tmpProject.Commands = new ObservableCollection<CommandData>();

                foreach (XElement tmpCommandXml in tmpProjectXml.Descendants(TAG_COMMAND))
                {
                    CommandData tmpChildCommand = CommandData.LoadFromXElement(tmpCommandXml);
                    tmpProject.Commands.Add(tmpChildCommand);
                }

                tmpResult.Add(tmpProject);
            }

            return tmpResult;
        }

        public void SaveProject(ConsoleProject argProject)
        {
            XDocument tmpConfigDoc = this.GetSettingsXmlDocument();
            this.AddProject(tmpConfigDoc, argProject);
            this.SaveSettingsDocument(tmpConfigDoc);
        }

        public void UpdateProject(string projectName, ConsoleProject argProject)
        {
            XDocument tmpConfigDoc = this.GetSettingsXmlDocument();
            this.RemoveProject(tmpConfigDoc, projectName);
            this.AddProject(tmpConfigDoc, argProject);
            this.SaveSettingsDocument(tmpConfigDoc);
        }

        public void DeleteProject(string argProjectName)
        {
            XDocument tmpConfigDoc = this.GetSettingsXmlDocument();
            this.RemoveProject(tmpConfigDoc, argProjectName);
            this.SaveSettingsDocument(tmpConfigDoc);
        }

        public void LoadGlobalSettings(out FontSettings argFontSettings, out IEnumerable<CommandData> argCommands)
        {
            this.logger.LogMessage("Loading global settings");

            XDocument tmpSettingsDocument = this.GetSettingsXmlDocument();
            argFontSettings = this.LoadFontSettings(tmpSettingsDocument);
            argCommands = this.LoadGlobalCommands(tmpSettingsDocument);
        }

        public async Task SaveFontSettingsAsync(FontSettings argCurrentFontSettings)
        {
            XDocument tmpProjectDocument = this.GetSettingsXmlDocument();
            XElement tmpHosterConfig = this.GetOrCreateHosterConfigElement(tmpProjectDocument);
            XElement tmpGlobalSettings = GetOrCreateChildElement(tmpHosterConfig, TAG_GLOBAL_SETTINGS);
            XElement tmpFontFamily = GetOrCreateChildElement(tmpGlobalSettings, TAG_FONT_FAMILY);

            tmpFontFamily.Value = argCurrentFontSettings.FontFamily;
            tmpFontFamily.SetAttributeValue(ATTRIBUTE_USE_BOLD_FONTS, argCurrentFontSettings.UseBoldFonts);
            await Task.Factory.StartNew(() => this.SaveSettingsDocument(tmpProjectDocument));
        }

        public async Task SaveGlobalCommands(IEnumerable<CommandData> argCommands)
        {
            XDocument tmpProjectDocument = this.GetSettingsXmlDocument();
            XElement tmpHosterConfig = this.GetOrCreateHosterConfigElement(tmpProjectDocument);
            XElement tmpGlobalCommands = GetOrCreateChildElement(tmpHosterConfig, TAG_GLOBAL_COMMANDS);

            tmpGlobalCommands.RemoveAll();
            foreach (CommandData tmpCommand in argCommands)
            {
                XElement tmpCommandXml = tmpCommand.ToXElement();
                tmpGlobalCommands.Add(tmpCommandXml);
            }

            await this.SaveSettingsDocumentAsync(tmpProjectDocument);
        }

        public IEnumerable<PluginDetails> LoadPlugins()
        {
            this.logger.LogMessage("Loading plugins...");

            IList<PluginDetails> tmpResult = null;

            XDocument tmpProjectsDocument = this.GetSettingsXmlDocument();
            foreach (XElement tmpPluginXml in tmpProjectsDocument.Descendants(TAG_PLUGINS))
            {
                if (tmpResult == null)
                {
                    tmpResult = new List<PluginDetails>();
                }

                tmpResult.Add(PluginDetails.LoadFromXElement(tmpPluginXml));
            }

            this.logger.LogMessage("Plugins loaded successfully");
            return tmpResult;
        }

        private void AddProject(XDocument tmpConfigDoc, ConsoleProject argProject)
        {
            XElement tmpProjectsNode;
            if ((tmpProjectsNode = tmpConfigDoc.Descendants(TAG_PROJECTS).SingleOrDefault()) == null)
            {
                tmpProjectsNode = new XElement(TAG_PROJECTS);
                tmpConfigDoc.Add(tmpProjectsNode);
            }

            XElement tmpProjectElement = argProject.ToXElement();
            tmpProjectsNode.Add(tmpProjectElement);

            AddProjectCommandsToXElement(argProject.Commands, tmpProjectElement);
        }

        private static void AddProjectCommandsToXElement(IEnumerable<CommandData> argCommands, XElement argRootElement)
        {
            if (argCommands != null && argCommands.Any())
            {
                XElement tmpCommandsElement = argRootElement.Descendants("Commands").SingleOrDefault();
                if (tmpCommandsElement == null)
                {
                    tmpCommandsElement = new XElement("Commands");
                    argRootElement.Add(tmpCommandsElement);
                }

                foreach (CommandData tmpCommand in argCommands)
                {
                    tmpCommandsElement.Add(tmpCommand.ToXElement());
                }
            }
        }

        private void RemoveProject(XDocument argDocument, string argProjectName)
        {
            XElement tmpProjectsNode;
            if ((tmpProjectsNode = argDocument.Descendants(TAG_PROJECTS).SingleOrDefault()) == null)
            {
                throw new ApplicationException("No such project");
            }

            XElement tmpCurrentProject = tmpProjectsNode.Descendants(TAG_PROJECT).Where(item => item.Attribute("name").Value.Equals(argProjectName, StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
            if (tmpCurrentProject == null)
            {
                throw new ApplicationException("No such project");
            }
            tmpCurrentProject.Remove();
        }

        private FontSettings LoadFontSettings(XDocument argConfigDocument)
        {
            FontSettings tmpResult = null;
            try
            {
                XElement tmpGlobalSettingsXml = argConfigDocument.Descendants(TAG_GLOBAL_SETTINGS).SingleOrDefault();
                if (tmpGlobalSettingsXml != null)
                {
                    XElement tmpFontXml = tmpGlobalSettingsXml.Descendants(TAG_FONT_FAMILY).LastOrDefault();
                    if (tmpFontXml != null)
                    {
                        bool tmpUseBoldFonts = false;
                        XAttribute tmpBoldFontAttribute = tmpFontXml.Attribute(ATTRIBUTE_USE_BOLD_FONTS);
                        if (tmpBoldFontAttribute != null)
                        {
                            Boolean.TryParse(tmpBoldFontAttribute.Value, out tmpUseBoldFonts);
                        }
                        tmpResult = new FontSettings(tmpFontXml.Value, tmpUseBoldFonts);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to load font settings", ex);
            }

            return tmpResult;
        }

        private IEnumerable<CommandData> LoadGlobalCommands(XDocument argConfigDocument)
        {
            IList<CommandData> tmpResult = null;
            try
            {
                tmpResult = new List<CommandData>();
                XElement tmpCommandsRootXml = argConfigDocument.Descendants(TAG_GLOBAL_COMMANDS).SingleOrDefault();
                if (tmpCommandsRootXml != null)
                {
                    foreach (XElement tmpCommandXml in tmpCommandsRootXml.Descendants(TAG_COMMAND))
                    {
                        tmpResult.Add(CommandData.LoadFromXElement(tmpCommandXml));
                    }
                }
            }
            catch (Exception ex)
            {
                throw new ApplicationException("Failed to load global commands", ex);
            }

            return tmpResult;
        }

        private static XElement GetOrCreateChildElement(XElement argParentElement, string argChildElementName)
        {
            XElement tmpFontFamily = argParentElement.Descendants(argChildElementName).SingleOrDefault();
            if (tmpFontFamily == null)
            {
                tmpFontFamily = new XElement(argChildElementName);
                argParentElement.Add(tmpFontFamily);
            }
            return tmpFontFamily;
        }

        private XElement GetOrCreateHosterConfigElement(XDocument tmpProjectDocument)
        {
            XElement tmpHosterConfig = tmpProjectDocument.Descendants(TAG_HOSTER_CONFIG).SingleOrDefault();
            if (tmpHosterConfig == null)
            {
                tmpHosterConfig = new XElement(TAG_HOSTER_CONFIG);
                tmpProjectDocument.AddFirst(tmpHosterConfig);
            }
            return tmpHosterConfig;
        }

        private XDocument GetOrCreateSettingsDocument()
        {
            XDocument tmpProjectDocument;
            if (!this.directoryContentProvider.FileExists(Constants.ProjectsFilename))
            {
                tmpProjectDocument = XDocument.Parse(@"<?xml version='1.0' encoding='utf-8' ?>
<HosterConfig>
  <Projects />
  <GenericCommands>
    <!--Command Name='dir' CommandText='dir' isFinal='true' />
    <Command Name='Root Level' CommandText='cd \' isFinal='true' />
    <Command Name='Level Up' CommandText='cd ..' isFinal='true' /-->
  </GenericCommands>
  <GlobalSettings>
  </GlobalSettings>
</HosterConfig>");
                this.SaveSettingsDocument(tmpProjectDocument);
            }
            else
            {
                tmpProjectDocument = this.GetSettingsXmlDocument();
            }
            return tmpProjectDocument;
        }

        private void SaveSettingsDocument(XDocument argXmlDocument)
        {
            this.xmlStorage.StoreXDocument(argXmlDocument, Constants.ProjectsFilename);
        }

        private async Task SaveSettingsDocumentAsync(XDocument argXmlDocument)
        {
            await Task.Factory.StartNew(() =>
                this.SaveSettingsDocument(argXmlDocument));
        }

        private XDocument GetSettingsXmlDocument(string argSettingsFile = Constants.ProjectsFilename)
        {
            if (!this.directoryContentProvider.FileExists(argSettingsFile))
            {
                throw new FileNotFoundException(argSettingsFile);
            }

            return this.xmlStorage.LoadXDocument(argSettingsFile);
        }
    }
}